/******************************************************************************/
package jamie.org.sil.www.usxUtils;

import java.io.*;
import java.lang.String.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.regex.*;
import java.util.TreeMap;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.*;


/******************************************************************************/
/**
   Bible information.
   <p>

   This provides access to details of scripture text from an XML
   configuration file.  Note that the methods provided here are
   concerned basically with providing details of the structure of the
   scripture text itself -- what books do we have, what are they known
   as in the vernacular language, etc?  We don't address the creation
   and parsing of references such as Jn 3:16 here because that turns
   out to be extremely complicated, and so there is a whole class --
   BibleReferenceHandler -- devoted with it.
   <p>

   Scripture configuration information comes essentially in three
   flavours ...
   <p>

   Some of it is concerned with the anatomy of the Bible as a whole --
   what books you have, how many there are, which belong to the Old
   Testament, which to the New Testament, etc.  This information is
   fixed forever, and is therefore built into the present class,
   rather than being taken from configuration files.  I have taken the
   (relatively) arbitrary decision to base it on the UBS classification
   scheme, which defines the full and abbreviated names which can be
   used, and also assigns a unique number to each book.  This scheme
   covers all the canonical books and also a standard collection of
   deutero-canonical books.  Note that the numbering scheme it uses
   does not run contiguously -- there are some gaps in it.
   <p>

   Some scripture configuration information is specific to a given
   vernacular -- ie the full (and optionally abbreviated) names of
   the books in that language.  This is picked up from configuration
   data -- the Config class identifies scriptureBook tags and passes
   them to the present class.
   <p>

   Some scripture configuration information is specific to the target
   software or device for which output is to be generated.  OliveTree,
   for instance, defines its own numbering and naming scheme.  This
   too is picked up from configuration files in the way just described.
   <p>

   And finally at the time of writing it is possible that there may be
   some configuration information which is particular to the state of
   the material available from the translator at a particular time.
   This information, for instance, might tell you whether or not text
   for a specific book exists.  I'm not currently sure whether I shall
   implement this, because I'm hoping to be able to determine this
   information simply by reference to the file system, and what files
   actually exist in it.
   <p>

   The present class interacts intimately with the Config class to ensure that
   all relevant data is loaded.  I won't go into details of that here, beyond
   saying that the present class will be ready for use once Config has finished
   initialising, but not before.
   <p>

   The class defines a large number of methods, most of which are concerned
   with converting between different forms of names (full and abbreviated,
   vernacular and English -- where "English" is synonymous with UBS); with
   returning information about individual books; or with validation.
   <p>

   Where these methods take book names as input parameters, they generally
   come in two flavours, one which takes a vernacular name as input, and one
   which takes an English (UBS) name.  The methods are distinguished by having
   "_V" and "_E" as a suffix to their names.  Note that I do not distinguish
   between abbreviated and full names -- you can pass either as input.  In
   general, you are likely to want to use the "_V" version in anything which
   is processing text supplied by the translators, and the "_E" version for
   administrative purposes (for example when working out file names).
   <p>

   (I <i>had</i> hoped not to need to distinguish between English and vernacular
   names, but this relied on the two never overlapping, and this hope was blown
   out of the water when I discovered that the UBS abbreviation for Jonah is
   JON, which is the same as the Bughotu full name for John.)

   Not <i>all</i> methods some in pairs.  Some take UBS book numbers as input,
   and therefore clearly are not affected by the English / vernacular split;
   and some currently seem likely to be wanted only in one of the two forms.
   Even where this is the case, however, if the method takes a book name as
   input, the method name still has the appropriate "_E" or "_V" suffix.
 */

public class BibleDetails implements SpecialistConfigConsumer
{
  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                               Public                                   **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  public static final String C_Ot_FirstBook = "GEN";
  public static final String C_Ot_LastBook  = "MAL";
  public static final String C_Nt_FirstBook = "MAT";
  public static final String C_Nt_LastBook  = "REV";
  public static final String C_Dc_FirstBook = "TOB";
  public static final String C_Dc_LastBook  = "DNT";


  /****************************************************************************/
  /**
     Singleton instance.

     @return Instance.
  */

  public static BibleDetails instance ()
  {
    if (null == m_Instance) m_Instance = new BibleDetails();
    return m_Instance;
  }


  /****************************************************************************/
  /**
     Returns the abbreviated or full English or vernacular name for a book,
     given the name in any form.  Note that if you ask for a vernacular
     abbreviation and none is available, you'll get back the full vernacular
     name instead.

     @param name English name of book (full or abbreviated).

     @param lang C_English or C_Vernacular.

     @param length C_Full or C_Abbreviated.

     @return Name.

     @throws ConverterException Details cannot be found.
  */

  public String getBookName_E (String englishName,
                               BibleReferenceSupport.Language lang, 
                               BibleReferenceSupport.Length length) throws ConverterException
  {
    return getBookName(getUbsBookNumber_E(englishName), lang, length);
  }


  /****************************************************************************/
  /**
     Returns the abbreviated or full English or vernacular name for a book,
     given the name in any form.  Note that if you ask for a vernacular
     abbreviation and none is available, you'll get back the full vernacular
     name instead.

     @param name Vernacular name of book (full or abbreviated).

     @param lang C_English or C_Vernacular.

     @param length C_Full or C_Abbreviated.

     @return Name.

     @throws ConverterException Details cannot be found.
  */

  public String getBookName_V (String vernacularName,
                               BibleReferenceSupport.Language lang,
                               BibleReferenceSupport.Length length) throws ConverterException
  {
    return getBookName(getUbsBookNumber_V(vernacularName), lang, length);
  }


  /****************************************************************************/
  /**
     Returns the abbreviated or full English or vernacular name for a book,
     given the name in any form.  Note that if you ask for a vernacular
     abbreviation and none is available, you'll get back the full vernacular
     name instead.

     @param ubsBookNo UBS Book number for book.

     @param lang C_English or C_Vernacular.

     @param length C_Full or C_Abbreviated.

     @return Name.

     @throws ConverterException Details cannot be found.
  */

  public String getBookName (int ubsBookNo,
                             BibleReferenceSupport.Language lang,
                             BibleReferenceSupport.Length length) throws ConverterException
  {
    switch (lang)
    {
      case C_English:
      {
        switch (length)
        {
	  case C_Abbreviated: return getAbbreviatedUbsName(ubsBookNo);
  	  case C_Full: return getFullUbsName(ubsBookNo);
        }
      }
      case C_Vernacular:
      {
        switch (length)
        {
	  case C_Abbreviated: return getAbbreviatedVernacularName(ubsBookNo);
  	  case C_Full: return getFullVernacularName(ubsBookNo);
        }
      }
    }

    throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "getBookName: invalid parameters");
  }


  /**************************************************************************/
  /**
     Return the number of chapters in a given book.

     @param englishName Full or abbreviated English name of book.

     @return Number of chapters.

     @throws ConverterException
  */

  public int getChapterCount_E (String englishName) throws ConverterException
  {
    String bookDetails = m_Versification.get(getFullUbsName_E(englishName));
    return bookDetails.length() / 4;
  }


  /**************************************************************************/
  /**
     Return the number of chapters in a given book.

     @param vernacularName Full or abbreviated vernacular name of book.

     @return Number of chapters.

     @throws ConverterException
  */

  public int getChapterCount_V (String vernacularName) throws ConverterException
  {
    String bookDetails = m_Versification.get(getFullUbsName_V(vernacularName));
    return bookDetails.length() / 4;
  }


  /****************************************************************************/
  /** When we're doing per-chapter text output, we have to allow for the
   *  possibility that sometimes we have only a partial collection of inputs.
   *  In these cases, we have to know the number of the first chapter we're
   *  going to encounter.
   *
   * @param fullFilePath
   *
   * @return First available chapter number.
   */

  public int getFirstAvailableChapterNumberForBook (String fullFilePath)
    throws FileNotFoundException, IOException, UnsupportedEncodingException
  {
    FileInputStream fileInputStream = new FileInputStream(fullFilePath);
    BufferedReader inputStreamReader = new BufferedReader(new InputStreamReader(fileInputStream, Config.instance().getEncoding()));
    int res = 0;
    String s = "";

    while (null != (s = inputStreamReader.readLine()))
    {
      s = s.trim();
      if (s.startsWith("<chapter "))
      {
        Matcher m = m_ChapterPattern.matcher(s);
        m.matches();
        res = Integer.parseInt(m.group(1));
        break;
      }
    }

    inputStreamReader.close();
    fileInputStream.close();

    return res;
  }


  /****************************************************************************/
  /**
     Given the name of a book (full or abbreviated, UBS or vernacular) and the
     name of a parameter, return the value of that parameter for that book.
     Note that values are always returned as strings, even where some other
     data type might be more appropriate.

     @param englishName English book name (full or abbreviated).
     
     @param parm Parameter name.
     
     @return UBS abbreviation.
     
     @throws ConverterException Book or parameter cannot be found.
  */

  public String getParameterForBook_E (String englishName, String parm) throws ConverterException
  {
    int i = getUbsBookNumber_E(englishName);
    if (m_UbsBookNumberToParameter.get(i).containsKey(parm))
      return m_UbsBookNumberToParameter.get(i).get(parm);
    throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "Can't find parameter " + parm + " for book " + englishName);
  }


  /****************************************************************************/
  /**
   * Get the UBS abbreviated name from a standard file name.
   *
   * @param fileName
   *
   * @return UBS name.
   */

  public String getAbbreviatedUbsNameFromFileName (String fileName)
  {
    return fileName.substring(2, 5);
  }


  /****************************************************************************/
  /**
     Convert any name (full or abbreviated, UBS or vernacular) to the
     corresponding UBS abbreviation.

     @param englishName English book name (full or abbreviated)
     @return UBS abbreviation.
     @throws ConverterException Book cannot be found.
  */

  public String getAbbreviatedUbsName_E (String englishName) throws ConverterException
  {
    if (m_AbbreviatedUbsNameToNumberMapping.containsKey(englishName))
      return m_UbsNumberToAbbreviatedNameMapping.get(m_AbbreviatedUbsNameToNumberMapping.get(englishName));

    else if (m_FullUbsNameToNumberMapping.containsKey(englishName))
      return m_UbsNumberToAbbreviatedNameMapping.get(m_FullUbsNameToNumberMapping.get(englishName));

    else
      throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "Can't find details for book " + englishName);
  }


  /****************************************************************************/
  /**
     Convert any name (full or abbreviated, UBS or vernacular) to the
     corresponding UBS abbreviation.

     @param vernacularName Vernacular book name (full or abbreviated)
     @return UBS abbreviation.
     @throws ConverterException Book cannot be found.
  */

  public String getAbbreviatedUbsName_V (String vernacularName) throws ConverterException
  {
    if (m_AbbreviatedVernacularNameToNumberMapping.containsKey(vernacularName))
        return m_UbsNumberToAbbreviatedNameMapping.get(m_AbbreviatedVernacularNameToNumberMapping.get(vernacularName));

    else if (m_FullVernacularNameToNumberMapping.containsKey(vernacularName))
      return m_UbsNumberToAbbreviatedNameMapping.get(m_FullVernacularNameToNumberMapping.get(vernacularName));

    else
      throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "Can't find details for book " + vernacularName);
  }


  /****************************************************************************/
  /**
     Convert the UBS book number to the abbreviated name.

     @param ubsBookNo
     @return UBS abbreviation.
  */

  public String getAbbreviatedUbsName (int ubsBookNo)
  {
    return m_UbsNumberToAbbreviatedNameMapping.get(ubsBookNo);
  }


  /**************************************************************************/
  /**
     Returns a comma-separated list of abbreviated UBS book names,
     appropriately ordered.

     @param firstName Name (in any form) of the first book of interest.

     @param lastName  Name (in any form) of the last book of interest.

     @return Comma-separated list of names.

     @throws ConverterException Look up failed.
  */

  public String getAbbreviatedUbsNames (String firstName, String lastName) throws ConverterException
  {
    int hi = getUbsBookNumber_E(lastName);
    int lo = getUbsBookNumber_E(firstName);
    String s = "";
    String sep = "";

    for (int i = lo; i <= hi; ++i)
      if (0 != C_UbsDetails[i][0].length()) { s += sep + C_UbsDetails[i][1]; sep = ",";}

    return s;
  }

  
  /****************************************************************************/
  /**
     Convert any name (full or abbreviated, UBS or vernacular) to the
     corresponding UBS full name.

     @param name English book name (full or abbreviated).
     @return UBS book number.
     @throws ConverterException Book cannot be found.
  */

  public int getUbsBookNumber_E (String englishName) throws ConverterException
  {
    return m_AbbreviatedUbsNameToNumberMapping.get(getAbbreviatedUbsName_E(englishName));
  }


  /****************************************************************************/
  /**
     Convert any name (full or abbreviated, UBS or vernacular) to the
     corresponding UBS full name.

     @param vernacularName Vernacular book name (full or abbreviated).
     @return UBS book number.
     @throws ConverterException Book cannot be found.
  */

  public int getUbsBookNumber_V (String vernacularName) throws ConverterException
  {
    return m_AbbreviatedUbsNameToNumberMapping.get(getAbbreviatedUbsName_V(vernacularName));
  }


  /****************************************************************************/
  /**
     Converts an English book name (full or abbreviated) to the corresponding
     full English name.

     @param englishName English book name (full or abbreviated).
     @return UBS abbreviation.
     @throws ConverterException Book cannot be found.
  */

  public String getFullUbsName_E (String englishName) throws ConverterException
  {
    if (m_FullUbsNameToNumberMapping.containsKey(englishName))
      return englishName;

    else if (m_AbbreviatedUbsNameToNumberMapping.containsKey(englishName))
      return m_UbsNumberToFullNameMapping.get(m_AbbreviatedUbsNameToNumberMapping.get(englishName));

    else
      throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "Can't find details for book " + englishName);
  }


  /****************************************************************************/
  /**
     Converts a vernacular book name (full or abbreviated) to the corresponding
     full English name.

     @param vernacularName Vernacular book name (full or abbreviated).
     @return UBS abbreviation.
     @throws ConverterException Book cannot be found.
  */

  public String getFullUbsName_V (String vernacularName) throws ConverterException
  {
    if (m_FullVernacularNameToNumberMapping.containsKey(vernacularName))
      return m_UbsNumberToFullNameMapping.get(m_FullVernacularNameToNumberMapping.get(vernacularName));

    else if (m_AbbreviatedVernacularNameToNumberMapping.containsKey(vernacularName))
      return m_UbsNumberToFullNameMapping.get(m_AbbreviatedVernacularNameToNumberMapping.get(vernacularName));

    else
      throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "Can't find details for book " + vernacularName);
  }


  /****************************************************************************/
  /**
     Convert the UBS book number to the UBS full name.

     @param ubsBookNo
     @return UBS full name
  */

  public String getFullUbsName (int ubsBookNo)
  {
    return m_UbsNumberToFullNameMapping.get(ubsBookNo);
  }


  /**************************************************************************/
  /**
     Returns a comma-separated list of full UBS book names, appropriately
     ordered.

     @param firstNameEnglish English name of first book, full or abbreviated.

     @param lastNameEnglish English name of first book, full or abbreviated.

     @return Comma-separated list of names.

     @throws ConverterException Look up failed.
  */

  public String getFullUbsNames_E (String firstNameEnglish, String lastNameEnglish) throws ConverterException
  {
    int hi = getUbsBookNumber_E(lastNameEnglish);
    int lo = getUbsBookNumber_E(firstNameEnglish);
    String s = "";
    String sep = "";

    for (int i = lo; i <= hi; ++i)
      if (0 != C_UbsDetails[i][0].length()) { s += sep + C_UbsDetails[i][0]; sep = ",";}

    return s;
  }

  
  /****************************************************************************/
  /**
     Convert full or abbreviated English name to the corresponding vernacular
     abbreviation (or to the full vernacular name if no abbreviation is defined).

     @param englishName English book name (full or abbreviated).
     @return Vernacular abbreviation (or full name if no abbreviation is available).
     @throws ConverterException Book cannot be found.
  */

  public String getAbbreviatedVernacularName_E (String englishName) throws ConverterException
  {
    int i = 0;

    if (m_AbbreviatedUbsNameToNumberMapping.containsKey(englishName))
      i = m_AbbreviatedUbsNameToNumberMapping.get(englishName);
    else if (m_FullUbsNameToNumberMapping.containsKey(englishName))
      i = m_FullUbsNameToNumberMapping.get(englishName);

    if (0 != i)
      return getAbbreviatedVernacularName(i);
    else
      throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "Can't find details for book " + englishName);
  }


  /****************************************************************************/
  /**
     Convert full or abbreviated vernacular name to the corresponding vernacular
     abbreviation (or to the full vernacular name if no abbreviation is defined).

     @param vernacularName Vernacular book name (full or abbreviated).
     @return Vernacular abbreviation (or full name if no abbreviation is available).
     @throws ConverterException Book cannot be found.
  */

  public String getAbbreviatedVernacularName_V (String vernacularName) throws ConverterException
  {
    if (m_AbbreviatedVernacularNameToNumberMapping.containsKey(vernacularName))
      return m_VernacularNumberToAbbreviatedNameMapping.get(m_AbbreviatedVernacularNameToNumberMapping.get(vernacularName));

    else if (m_FullVernacularNameToNumberMapping.containsKey(vernacularName))
    {
      int i = m_FullVernacularNameToNumberMapping.get(vernacularName);
      if (m_VernacularNumberToAbbreviatedNameMapping.containsKey(i))
        return m_VernacularNumberToAbbreviatedNameMapping.get(i);
      else
        return vernacularName;
    }

      throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "Can't find details for book " + vernacularName);
  }


  /****************************************************************************/
  /**
     Convert the UBS book number to the abbreviated vernacular name (or to
     the full vernacular name if no abbreviation is available).

     @param ubsBookNo
     @return Vernacular abbreviated name
  */

  public String getAbbreviatedVernacularName (int ubsBookNo)
  {
    try
    {
      String s = m_VernacularNumberToAbbreviatedNameMapping.get(ubsBookNo);
      if (null != s) return s;
    }
    catch(Exception e)
    {
    }

    try
    {
      String s = m_VernacularNumberToFullNameMapping.get(ubsBookNo);
      if (null != s) return s;
      throw new Exception();
    }
    catch(Exception e)
    {
      throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "Can't find details for book " + ubsBookNo);
    }
  }


  /****************************************************************************/
  /**
     Convert any name (full or abbreviated, UBS or vernacular) to the
     corresponding UBS abbreviation.

     @param englishName English name (full or abbreviated).
     @return UBS abbreviation.
     @throws ConverterException Book cannot be found.
  */

  public String getFullVernacularName_E (String englishName) throws ConverterException
  {
    if (m_FullUbsNameToNumberMapping.containsKey(englishName))
      return m_VernacularNumberToFullNameMapping.get(m_FullUbsNameToNumberMapping.get(englishName));

    else if (m_AbbreviatedUbsNameToNumberMapping.containsKey(englishName))
      return m_VernacularNumberToFullNameMapping.get(m_AbbreviatedUbsNameToNumberMapping.get(englishName));

    else
      throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "Can't find details for book " + englishName);
  }


  /****************************************************************************/
  /**
     Convert any name (full or abbreviated, UBS or vernacular) to the
     corresponding UBS abbreviation.

     @param vernacularName Vernacular name (full or abbreviated).
     @return UBS abbreviation.
     @throws ConverterException Book cannot be found.
  */

  public String getFullVernacularName_V (String vernacularName) throws ConverterException
  {
    if (m_FullVernacularNameToNumberMapping.containsKey(vernacularName))
      return vernacularName;

    else if (m_AbbreviatedVernacularNameToNumberMapping.containsKey(vernacularName))
      return m_VernacularNumberToFullNameMapping.get(m_AbbreviatedVernacularNameToNumberMapping.get(vernacularName));

    else
      throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "Can't find details for book " + vernacularName);
  }


  /****************************************************************************/
  /**
     Convert the UBS book number to the full vernacular name.

     @param ubsBookNo
     @return Vernacular full name
  */

  public String getFullVernacularName (int ubsBookNo)
  {
    try
    {
      return m_VernacularNumberToFullNameMapping.get(ubsBookNo);
    }
    catch(Exception e)
    {
      throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "Can't find details for book " + ubsBookNo);
    }
  }


  /****************************************************************************/
  /**
     Returns an array of UBS full and abbreviated book names.
     
     @return Book names.
  */
  
  public String[] getFullAndAbbreviatedVernacularNamesAsArray ()
  {
    Object[] x1 = m_FullVernacularNameToNumberMapping.keySet().toArray();
    Object[] x2 = m_AbbreviatedVernacularNameToNumberMapping.keySet().toArray();
    String[] res = new String[x1.length + x2.length];
    int j = 0;
    for (int i = 0; i < x1.length; ++i) res[j++] = (String) x1[i];
    for (int i = 0; i < x2.length; ++i) res[j++] = (String) x2[i];
    return res;
  }


  /****************************************************************************/
  /**
     Return the number of verses in a given chapter in a given book.

     @param englishName English book name, full or abbreviated.

     @param chapter 1-based chapter number.

     @return Number of chapters.
  */

  public int getVerseCount_E (String englishName, int chapter) throws ConverterException
  {
    String bookDetails = m_Versification.get(getFullUbsName_E(englishName));
    String s = bookDetails.substring(4 * (chapter - 1), 4 * chapter);
    return Integer.parseInt(s.trim());
  }


  /****************************************************************************/
  /**
     Return the number of verses in a given chapter in a given book.

     @param verncularName Vernacular book name, full or abbreviated.

     @param chapter 1-based chapter number.

     @return Number of chapters.
  */

  public int getVerseCount_V (String vernacularName, int chapter) throws ConverterException
  {
    String bookDetails = m_Versification.get(getFullUbsName_V(vernacularName));
    String s = bookDetails.substring(4 * (chapter - 1), 4 * chapter);
    return Integer.parseInt(s.trim());
  }


  /**************************************************************************/
  /**
     Checks whether a given book is in the given portion of the Bible.

     @param englishFirstName English name of first book in portion of interest
                             (full or abbreviated).

     @param englishLastName  English name of first book in portion of interest
                             (full or abbreviated).


     @param englishName      English name of book to check (full or abbreviated)

     @return True if the book is in the given portion.

     @throws ConverterException
  */

  public boolean isInBiblePortion_E (String englishFirstName,
                                     String englishLastName,
                                     String englishName) throws ConverterException
  {
    int i = getUbsBookNumber_E(englishName);
    return i >= getUbsBookNumber_E(englishFirstName) && i <= getUbsBookNumber_E(englishLastName);
  }


  /**************************************************************************/
  /**
     Checks whether a given string is a valid English book name (English
     or vernacular, full or abbreviated).

     @param englishName English name to be checked (full or abbreviated).

     @return True if the name is valid.
  */

  public Boolean isValidBookName_E (String englishName)
  {
    try
    {
      getAbbreviatedUbsName_E(englishName);
      return true;
    }
    catch (Exception e)
    {
      return false;
    }
  }


  /**************************************************************************/
  /**
     Checks whether a given string is a valid English book name (English
     or vernacular, full or abbreviated).

     @param vernacularName Vernacular name to be checked (full or abbreviated).

     @return True if the name is valid.
  */

  public Boolean isValidBookName_V (String vernacularName)
  {
    try
    {
      getAbbreviatedUbsName_V(vernacularName);
      return true;
    }
    catch (Exception e)
    {
      return false;
    }
  }


  /**************************************************************************/
  /**
     Checks whether a given reference is valid.

     @param englishName English book name (full or abbreviated).

     @param chapter 1-based chapter number.

     @param verse 1-based verse number.

     @return Empty string if the reference is valid, otherwise an error message.
  */

  public String isValidReference_E (String englishName, int chapter, int verse)
  {
    try { englishName = getFullUbsName_E(englishName); } catch(Exception e) { return "Invalid book"; }

    if (!m_Versification.containsKey(englishName)) return "Invalid book";

    int nChapters = getChapterCount_E(englishName);

    if (chapter < 1 || chapter > nChapters) return "Invalid chapter " + chapter;

    int nVerses = getVerseCount_E(englishName, chapter);

    if (verse < 1 || verse > nVerses) return "Invalid verse " + verse;

    return "";
  }


  /**************************************************************************/
  /**
     Checks whether a given reference is valid.

     @param englishName English book name (full or abbreviated).

     @param chapter 1-based chapter number.

     @param verse 1-based verse number.

     @return Empty string if the reference is valid, otherwise an error message.
  */

  public String isValidReference_V (String vernacularName, int chapter, int verse)
  {
    try { vernacularName = getFullUbsName_V(vernacularName); } catch(Exception e) { return "Invalid book"; }

    if (!m_Versification.containsKey(vernacularName)) return "Invalid book";

    int nChapters = getChapterCount_V(vernacularName);

    if (chapter < 1 || chapter > nChapters) return "Invalid chapter " + chapter;

    int nVerses = getVerseCount_V(vernacularName, chapter);

    if (verse < 1 || verse > nVerses) return "Invalid verse " + verse;

    return "";
  }


  /****************************************************************************/
  /** Loads config data from the given file. (This method is called from
   *  Config, by dint of the present class implementing the
   *  SpecialistConfigConsumer interface.)
   *
   * @param fileName
   * @param type Type of config information available from this file.
   * @throws ConverterException
   * @throws IOException
   * @throws javax.xml.parsers.ParserConfigurationException
   * @throws org.xml.sax.SAXException
   */

  @Override public void load (String fileName, String type) throws ConverterException,
                                                                   IOException,
                                                                   javax.xml.parsers.ParserConfigurationException,
                                                                   org.xml.sax.SAXException
  {
    loadVersification(fileName);
  }


  /****************************************************************************/
  /**
   * Reads book details from all the various tags available from Config.
   * Note that this should not be called until Config has finished reading all
   * files, for which reason it should be called by Config itself.
   *
   * @throws ConverterException No data found.
   */

  public void loadBookDetails (ArrayList<Config.ScriptureBook> bookList) throws ConverterException
  {
    for (Config.ScriptureBook book : bookList) loadBookDetails1(book);
  }


  /**************************************************************************/
  /**
     Used in combination with {@link #validateVersesInChapterStart}, qv.

     @param thunk  Thunk containing details to be validated.

     @param verseNo 1-based verse number.
  */

  public void validateVersesInChapterAddVerse (Object thunk, int verseNo)
  {
    ((ValidateVersesThunk) thunk).AddVerse(verseNo);
  }


  /**************************************************************************/
  /**
     Used in combination with {@link #validateVersesInChapterStart}, qv.

     @param thunk  Thunk containing details to be validated.
   *
   * @return String detailing error, or empty string on success.

     @throws ConverterException
  */

  public String validateVersesInChapterEnd (Object thunk) throws ConverterException
  {
    return ((ValidateVersesThunk) thunk).Check();
  }


  /**************************************************************************/
  /**
     Used in combination with {@link #validateVersesInChapterEnd} and
     {@link #validateVersesInChapterAddVerse} to check that we have
     details for all the verses in a given chapter, and that they are
     all in order with no duplication.

     Call the present routine to kick the process off.  This returns an
     opaque thunk which the caller should pass to the other routines in
     the collection.  See also <code>ValidateVersesThunk</code>.

     @param englishName English book name (full or abbreviated).

     @param chapter 1-based chapter number.

     @return Opaque thunk with the caller should pass on for further validation.
  */

  public Object validateVersesInChapterStart (String englishName, int chapter) throws ConverterException
  {
    englishName = getFullUbsName_E(englishName);

    if (!m_Versification.containsKey(englishName)) throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "Invalid book identifier for versification: " + englishName);
    int nChapters = getChapterCount_E(englishName);
    if (chapter < 1 || chapter > nChapters) throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "Chapter out of range: " + englishName + " " + chapter);

    return (Object) new ValidateVersesThunk(englishName, chapter, getVerseCount_E(englishName, chapter));
  }


  /**************************************************************************/
  /**
     Used in combination with {@link #validateVersesInChapterEnd} and
     {@link #validateVersesInChapterAddVerse} to check that we have
     details for all the verses in a given chapter, and that they are
     all in order with no duplication.

     Call the present routine to kick the process off.  This returns an
     opaque thunk which the caller should pass to the other routines in
     the collection.  See also <code>ValidateVersesThunk</code>.

     @param vernacularName Vernacular book name (full or abbreviated).

     @param chapter 1-based chapter number.

     @return Opaque thunk with the caller should pass on for further validation.
  */

  public Object validateVersesInChapterStart_V (String vernacularName, int chapter) throws ConverterException
  {
    vernacularName = getFullUbsName_V(vernacularName);

    if (!m_Versification.containsKey(vernacularName)) throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "Invalid book identifier for versification: " + vernacularName);
    int nChapters = getChapterCount_V(vernacularName);
    if (chapter < 1 || chapter > nChapters) throw new ConverterException(ConverterException.Action.AbandonFileWhenever, ConverterException.Log.Error, "Chapter out of range: " + vernacularName + " " + chapter);

    return (Object) new ValidateVersesThunk(vernacularName, chapter, getVerseCount_V(vernacularName, chapter));
  }





  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                               Private                                  **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /**
     Constructor.
  */

  private BibleDetails ()
  {
    for (int i = 0; i < C_UbsDetails.length; ++i)
    {
      m_UbsBookNumberToParameter.add(new TreeMap<String, String>(m_CaseInsensitiveComparator));

      if (0 != C_UbsDetails[i][0].length())
      {
        m_FullUbsNameToNumberMapping.put(C_UbsDetails[i][0], i);
        m_UbsNumberToFullNameMapping.put(i, C_UbsDetails[i][0]);
        m_AbbreviatedUbsNameToNumberMapping.put(C_UbsDetails[i][1], i);
        m_UbsNumberToAbbreviatedNameMapping.put(i, C_UbsDetails[i][1]);
      }
    }
  }


  /****************************************************************************/
  /* Support routine for loadBookDetails, qv. */

  public void loadBookDetails1 (Config.ScriptureBook book) throws ConverterException
  {
    String ubsAbbreviatedName = book.getUbsAbbreviation();
    if (0 == ubsAbbreviatedName.length()) throw new ConverterException(ConverterException.Action.AbandonRunWhenever, ConverterException.Log.Error, "scriptureBook node with no ubsAbbreviation");
    int ubsBookNo = getUbsBookNumber_E(ubsAbbreviatedName);

    String parm = book.getParm();
    if (0 == parm.length()) throw new ConverterException(ConverterException.Action.AbandonRunWhenever, ConverterException.Log.Error, "scriptureBook node with no parm");

    String val = book.getValue();

    if (0 != val.length())
    {
      if ("vernacularFullName".equalsIgnoreCase(parm))
      {
        m_FullVernacularNameToNumberMapping.put(val, ubsBookNo);
        m_VernacularNumberToFullNameMapping.put(ubsBookNo, val);
      }
      else if ("vernacularAbbreviatedName".equalsIgnoreCase(parm))
      {
        m_AbbreviatedVernacularNameToNumberMapping.put(val, ubsBookNo);
        m_VernacularNumberToAbbreviatedNameMapping.put(ubsBookNo, val);
      }
      else
      {
        m_UbsBookNumberToParameter.get(ubsBookNo).put(parm, val);
      }
    }
  }


  /****************************************************************************/
  /* Loads versification details, first deciding whether we have a file in our
     own homebrew XML format, or a Paratext .vrs file. */

  private void loadVersification (String fileName) throws ConverterException,
                                                          IOException,
                                                          javax.xml.parsers.ParserConfigurationException,
                                                          org.xml.sax.SAXException
  {
    FileReader f = new FileReader(fileName);
    BufferedReader b = new BufferedReader(f);
    String s = null;
    boolean xml = false;
      
    while (null != (s = b.readLine()))
    {
      xml = s.contains("<?xml");
      if (xml) break;
    }
    
    b.close();
    f.close();
    
    if (xml)
      loadVersificationXml(fileName);
    else
      loadVersificationVrs(fileName);
  }

  
  /****************************************************************************/
  /* Loads versification details. */

  private void loadVersificationVrs (String fileName) throws ConverterException,
                                                             IOException
  {
    FileReader f = new FileReader(fileName);
    BufferedReader b = new BufferedReader(f);
    String s = null;
    
    while (null != (s = b.readLine()))
    {
      s = s.trim();
      if (0 == s.length()) continue;       // Skip blanks.
      if (!s.startsWith("#")) continue;    // Skip comments.
      if (-1 != s.indexOf("=")) continue;  // Skip mapping section.
      String[] elements = s.split("\\s+");
      String book = elements[0];
      
      s = "";
      for (int i = 1; i < elements.length; ++i)
      {
        int lastVerse = Integer.parseInt(elements[i].split("\\:")[1]);
        s = s + String.format("%4d", lastVerse);
      }

      m_Versification.put(BibleDetails.instance().getFullUbsName_E(book), s);
    }

    
    b.close();
    f.close();
  }
  
  
  /****************************************************************************/
  /* Loads versification details from our homebrew XML file format. */

  private void loadVersificationXml (String fileName) throws ConverterException,
                                                             IOException,
                                                             javax.xml.parsers.ParserConfigurationException,
                                                             org.xml.sax.SAXException
   {
     System.setProperty("javax.xml.parsers.DocumentBuilderFactory",
                       "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");

    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(true);
    //factory.setXIncludeAware(true);

    DocumentBuilder builder = factory.newDocumentBuilder();
    Document doc = builder.parse(new File(fileName));
    doc.getDocumentElement().normalize();
    try
    {
      parseVersification(doc);
    }
    catch (Exception e)
    {
      throw new ConverterException(ConverterException.Action.AbandonRunWhenever, ConverterException.Log.Error, e.getMessage());
    }
  }


  /**************************************************************************/
  /**
     Parses versification details.
  */

  private void parseVersification (Document doc)
  {
    NodeList nodeList = doc.getElementsByTagName("books");
    NodeList bookList = ((Element) nodeList.item(0)).getElementsByTagName("book");

    for (int j = 0; j < bookList.getLength(); ++j)
    {
      String bookName = bookList.item(j).getAttributes().getNamedItem("name").getNodeValue();
      NodeList chapterList = ((Element) bookList.item(j)).getElementsByTagName("chapter");
      String s = "";

      for (int k = 0; k < chapterList.getLength(); ++k)
      {
        Node n = chapterList.item(k);
        //int chapterNo = Integer.parseInt(n.getAttributes().getNamedItem("n").getNodeValue());
        int lastVerse = Integer.parseInt(n.getAttributes().getNamedItem("verses").getNodeValue());
        s = s + String.format("%4d", lastVerse);
      }

      m_Versification.put(bookName, s);
    }
  }






  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                               Private                                  **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /**************************************************************************/
  private class CaseInsensitiveComparator implements Comparator
  {
    @Override public int compare (Object a, Object b)
    {
      return ((String) a).compareToIgnoreCase((String) b);
    }
  }

  
  /****************************************************************************/
  /* See comments to validateVersesInChapterStart. */

  private class ValidateVersesThunk
  {
    private String m_BookName;
    private int m_ChapterNo;
    private String m_Error = "";
    private int m_Verse = 0;
    private int m_VerseCount;

    private ValidateVersesThunk (String bookName, int chapterNo, int verseCount)
    {
      m_BookName = bookName;
      m_ChapterNo = chapterNo;
      m_VerseCount = verseCount;
    }

    private void AddVerse (int verseNo)
    {
      if (verseNo != m_Verse + 1)
      {
        if (0 == m_Error.length()) m_Error = "Verses missing or out of sequence at or around verse " + m_ChapterNo + "." + verseNo;
      }
      else if (verseNo > m_VerseCount && Config.instance().getCheckVersification())
      {
        if (0 == m_Error.length()) m_Error = "Too many verses";
      }
      else
	m_Verse = verseNo;
    }

    private String Check ()
    {
      if (m_Verse < m_VerseCount && 0 == m_Error.length()  && Config.instance().getCheckVersification()) m_Error = "Too few verses";
      return (0 == m_Error.length()) ? "" : m_BookName + " chapter " + m_ChapterNo + ": " + m_Error;
    }
  }


  /****************************************************************************/
  /**
     A complete list of full English (UBS) booknames, in order.  Note that
     this follows the UBS numbering scheme, so I need to introduce some
     dummy entries.  The nth entry gives the details for book n in the
     UBS numbering scheme.  Each entry comprises the full name of the book,
     followed by the abbreviated name.
  */
  
  private static final String [][] C_UbsDetails =
  {
    {"",                              "  0"}, // UBS skips here.
    {"Genesis",                       "GEN"}, // 1
    {"Exodus",                        "EXO"},
    {"Leviticus",                     "LEV"},
    {"Numbers",                       "NUM"},
    {"Deuteronomy",                   "DEU"},
    {"Joshua",                        "JOS"},
    {"Judges",                        "JDG"},
    {"Ruth",                          "RUT"},
    {"1 Samuel",                      "1SA"},
    {"2 Samuel",                      "2SA"},
    {"1 Kings",                       "1KI"},
    {"2 Kings",                       "2KI"},
    {"1 Chronicles",                  "1CH"},
    {"2 Chronicles",                  "2CH"},
    {"Ezra",                          "EZR"},
    {"Nehemiah",                      "NEH"},
    {"Esther",                        "EST"},
    {"Job",                           "JOB"},
    {"Psalms",                        "PSA"},
    {"Proverbs",                      "PRO"},
    {"Ecclesiastes",                  "ECC"},
    {"Song of Solomon",               "SNG"},
    {"Isaiah",                        "ISA"},
    {"Jeremiah",                      "JER"},
    {"Lamentations",                  "LAM"},
    {"Ezekiel",                       "EZK"},
    {"Daniel",                        "DAN"},
    {"Hosea",                         "HOS"},
    {"Joel",                          "JOL"},
    {"Amos",                          "AMO"},
    {"Obadiah",                       "OBA"},
    {"Jonah",                         "JON"},
    {"Micah",                         "MIC"},
    {"Nahum",                         "NAM"},
    {"Habakkuk",                      "HAB"},
    {"Zephaniah",                     "ZEP"},
    {"Haggai",                        "HAG"},
    {"Zechariah",                     "ZEC"},
    {"Malachi",                       "MAL"}, // 39
    {"",                              " 40"}, // UBS skips here.
    {"Matthew",                       "MAT"}, // 41
    {"Mark",                          "MRK"},
    {"Luke",                          "LUK"},
    {"John",                          "JHN"},
    {"Acts",                          "ACT"},
    {"Romans",                        "ROM"},
    {"1 Corinthians",                 "1CO"},
    {"2 Corinthians",                 "2CO"},
    {"Galatians",                     "GAL"},
    {"Ephesians",                     "EPH"},
    {"Philippians",                   "PHP"},
    {"Colossians",                    "COL"},
    {"1 Thessalonians",               "1TH"},
    {"2 Thessalonians",               "2TH"},
    {"1 Timothy",                     "1TI"},
    {"2 Timothy",                     "2TI"},
    {"Titus",                         "TIT"},
    {"Philemon",                      "PHM"},
    {"Hebrews",                       "HEB"},
    {"James",                         "JAS"},
    {"1 Peter",                       "1PE"},
    {"2 Peter",                       "2PE"},
    {"1 John",                        "1JN"},
    {"2 John",                        "2JN"},
    {"3 John",                        "3JN"},
    {"Jude",                          "JUD"},
    {"Revelation",                    "REV"}, // 67
    {"Tobit",                         "TOB"}, // 68
    {"Judith",                        "JDT"}, // 69
    {"Additions to Esther (Greek)",   "ESG"}, // 70
    {"Wisdom of Solomon",             "WIS"}, // 71
    {"Sirach",                        "SIR"}, // 72
    {"Baruch",                        "BAR"}, // 73
    {"Epistle of Jeremiah",           "LJE"}, // 74
    {"Song of the Three Young Men",   "S3Y"}, // 75
    {"Susannah",                      "SUS"}, // 76
    {"Bel and the Dragon",            "BEL"}, // 77
    {"1 Maccabees",                   "1MA"}, // 78
    {"2 Maccabees",                   "2MA"}, // 79
    {"3 Maccabees",                   "3MA"}, // 80
    {"4 Maccabees",                   "4MA"}, // 81
    {"1 Esdras",                      "1ES"}, // 82
    {"2 Esdras",                      "2ES"}, // 83
    {"Prayer of Manasseh",            "MAN"}, // 84
    {"",                              " 85"}, // UBS skips here.
    {"Prayer of Azariah",             "ODA"}, // 86
    {"Psalm 151 (Psalm of Solomon)",  "PSS"}, // 87
    {"",                              " 88"}, // UBS skips here.
    {"",                              " 89"}, // UBS skips here.
    {"",                              " 90"}, // UBS skips here.
    {"",                              " 91"}, // UBS skips here.
    {"Additions to Daniel",           "DNT"}  // 92
  };


  /****************************************************************************/
  private static final Pattern m_ChapterPattern = Pattern.compile(".*number\\s*\\=\\s*[\\'\\\"](\\d+)[\\'\\\"].*");
  

  /****************************************************************************/
  private static BibleDetails m_Instance = null;


  /**************************************************************************/
  private final CaseInsensitiveComparator m_CaseInsensitiveComparator = new CaseInsensitiveComparator();


  
  /**************************************************************************/
  /* Mappings from book names to other details, etc. */

  private TreeMap<String, Integer>  m_AbbreviatedUbsNameToNumberMapping = new TreeMap<String, Integer>(m_CaseInsensitiveComparator);
  private TreeMap<String, Integer>  m_FullUbsNameToNumberMapping = new TreeMap<String, Integer>(m_CaseInsensitiveComparator);
  private TreeMap<Integer, String>  m_UbsNumberToAbbreviatedNameMapping = new TreeMap<Integer, String>();
  private TreeMap<Integer, String>  m_UbsNumberToFullNameMapping = new TreeMap<Integer, String>();
  private TreeMap<String, Integer>  m_AbbreviatedVernacularNameToNumberMapping = new TreeMap<String, Integer>(m_CaseInsensitiveComparator);
  private TreeMap<String, Integer>  m_FullVernacularNameToNumberMapping = new TreeMap<String, Integer>(m_CaseInsensitiveComparator);
  private TreeMap<Integer, String>  m_VernacularNumberToAbbreviatedNameMapping = new TreeMap<Integer, String>();
  private TreeMap<Integer, String>  m_VernacularNumberToFullNameMapping = new TreeMap<Integer, String>();
  private ArrayList<TreeMap<String, String>> m_UbsBookNumberToParameter = new ArrayList<TreeMap<String, String>>();


  /**************************************************************************/
  /* Versification scheme. */
  
  private TreeMap<String, String> m_Versification = new TreeMap<String, String>(m_CaseInsensitiveComparator);
}
